{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[this doc on github](https://github.com/dotnet/interactive/tree/main/samples/notebooks/csharp/Samples)\n",
    "\n",
    "# .NET Interactive report \n",
    "\n",
    "project report for [.NET Interactive repo](https://github.com/dotnet/interactive)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Setup\n",
    "Importing pacakges and setting up connection"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "#r \"nuget: NodaTime, 3.1.0\"\n",
    "#r \"nuget: Octokit, 0.51.0\"\n",
    "#r \"nuget: Plotly.NET, 2.0.0\"\n",
    "#r \"nuget: Plotly.NET.Interactive, 2.0.0\"\n",
    "\n",
    "using Microsoft.FSharp.Core;\n",
    "using static Microsoft.DotNet.Interactive.Formatting.PocketViewTags;\n",
    "using Microsoft.DotNet.Interactive.Formatting;\n",
    "using Octokit;\n",
    "using NodaTime;\n",
    "using NodaTime.Extensions;\n",
    "using Plotly.NET;\n",
    "using Plotly.NET.LayoutObjects;"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "var organization = \"dotnet\";\n",
    "var repositoryName = \"interactive\";\n",
    "var options = new ApiOptions();\n",
    "var gitHubClient = new GitHubClient(new ProductHeaderValue(\"notebook\"));"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[Generate a user token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) to get rid of public [api](https://github.com/octokit/octokit.net/blob/master/docs/getting-started.md) throttling policies for anonymous users "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "var tokenAuth = new Credentials(\"your token\");\n",
    "gitHubClient.Credentials = tokenAuth;"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "var today = SystemClock.Instance.InUtc().GetCurrentDate();\n",
    "var startOfTheMonth = today.With(DateAdjusters.StartOfMonth);\n",
    "var startOfPreviousMonth = today.With(DateAdjusters.StartOfMonth) - Period.FromMonths(1);\n",
    "var startOfTheYear = new LocalDate(today.Year, 1, 1).AtMidnight();\n",
    "\n",
    "var currentYearIssuesRequest = new RepositoryIssueRequest {\n",
    "     State = ItemStateFilter.All,\n",
    "     Since = startOfTheYear.ToDateTimeUnspecified()\n",
    "};\n",
    "\n",
    "var pullRequestRequest = new PullRequestRequest {\n",
    "    State = ItemStateFilter.All\n",
    "};"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Perform github queries"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "#!time\n",
    "var branches = await gitHubClient.Repository.Branch.GetAll(organization, repositoryName);\n",
    "var pullRequests = await gitHubClient.Repository.PullRequest.GetAllForRepository(organization, repositoryName, pullRequestRequest);\n",
    "var forks = await gitHubClient.Repository.Forks.GetAll(organization, repositoryName);\n",
    "var currentYearIssues = await gitHubClient.Issue.GetAllForRepository(organization, repositoryName, currentYearIssuesRequest);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Branch data"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Pull request data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "var pullRequestCreatedThisMonth = pullRequests.Where(pr => pr.CreatedAt > startOfTheMonth.ToDateTimeUnspecified());\n",
    "var pullRequestClosedThisMonth =pullRequests.Where(pr => (pr.MergedAt != null && pr.MergedAt > startOfTheMonth.ToDateTimeUnspecified()));\n",
    "var contributorsCount = pullRequestClosedThisMonth.GroupBy(pr => pr.User.Login);\n",
    "\n",
    "var pullRequestLifespan = pullRequests.GroupBy(pr =>\n",
    "            {\n",
    "                var lifeSpan = (pr.ClosedAt ?? today.ToDateTimeUnspecified()) - pr.CreatedAt;\n",
    "                return Math.Max(0, Math.Ceiling(lifeSpan.TotalDays));\n",
    "            })\n",
    "            .Where(g => g.Key > 0)\n",
    "            .OrderBy(g => g.Key)\n",
    "            .ToDictionary(g => g.Key, g => g.Count());"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fork data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "var forkCreatedThisMonth = forks.Where(fork => fork.CreatedAt >= startOfTheMonth.ToDateTimeUnspecified());\n",
    "var forkCreatedPreviousMonth = forks.Where(fork => (fork.CreatedAt >= startOfPreviousMonth.ToDateTimeUnspecified()) && (fork.CreatedAt < startOfTheMonth.ToDateTimeUnspecified()));\n",
    "var forkCreatedByMonth = forks.GroupBy(fork => new DateTime(fork.CreatedAt.Year, fork.CreatedAt.Month, 1));\n",
    "var forkUpdateByMonth = forks.GroupBy(f => new DateTime(f.UpdatedAt.Year, f.UpdatedAt.Month,  1) ).Select(g => new {Date = g.Key, Count = g.Count()}).OrderBy(g => g.Date).ToArray();\n",
    "var total = 0;\n",
    "var forkCountByMonth = forkCreatedByMonth.OrderBy(g => g.Key).Select(g => new {Date = g.Key, Count = total += g.Count()}).ToArray();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Issues data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "bool IsBug(Issue issue){\n",
    "    return issue.Labels.FirstOrDefault(l => l.Name == \"bug\")!= null;\n",
    "}\n",
    "\n",
    "bool TargetsArea(Issue issue){\n",
    "    return issue.Labels.FirstOrDefault(l => l.Name.StartsWith(\"Area-\"))!= null;\n",
    "}\n",
    "\n",
    "string GetArea(Issue issue){\n",
    "    return issue.Labels.FirstOrDefault(l => l.Name.StartsWith(\"Area-\"))?.Name;\n",
    "}\n",
    "\n",
    "var openIssues = currentYearIssues.Where(IsBug).Where(issue => issue.State == \"open\");\n",
    "var closedIssues = currentYearIssues.Where(IsBug).Where(issue => issue.State == \"closed\");\n",
    "var oldestIssues = openIssues.OrderBy(issue => today.ToDateTimeUnspecified() - issue.CreatedAt).Take(20);\n",
    "var createdCurrentMonth = currentYearIssues.Where(IsBug).Where(issue => issue.CreatedAt >= startOfTheMonth.ToDateTimeUnspecified());\n",
    "var createdPreviousMonth = currentYearIssues.Where(IsBug).Where(issue => (issue.CreatedAt >= startOfPreviousMonth.ToDateTimeUnspecified()) && (issue.CreatedAt < startOfTheMonth.ToDateTimeUnspecified()));\n",
    "var openFromPreviousMonth = openIssues.Where(issue => (issue.CreatedAt > startOfPreviousMonth.ToDateTimeUnspecified()) && (issue.CreatedAt < startOfTheMonth.ToDateTimeUnspecified()));\n",
    "var createdByMonth = currentYearIssues.Where(IsBug).GroupBy(issue => new DateTime(issue.CreatedAt.Year, issue.CreatedAt.Month, 1)).OrderBy(g=>g.Key).ToDictionary(g => g.Key, g => g.Count());\n",
    "var closedByMonth = closedIssues.GroupBy(issue => new DateTime((int) issue.ClosedAt?.Year, (int) issue.ClosedAt?.Month, 1)).OrderBy(g=>g.Key).ToDictionary(g => g.Key, g => g.Count());\n",
    "var openIssueAge = openIssues.GroupBy(issue => new DateTime(issue.CreatedAt.Year, issue.CreatedAt.Month, issue.CreatedAt.Day)).ToDictionary(g => g.Key, g => g.Max(issue =>Math.Max(0, Math.Ceiling( (today.ToDateTimeUnspecified() - issue.CreatedAt).TotalDays))));\n",
    "var openByMonth = new Dictionary<DateTime, int>();\n",
    "var minDate = createdByMonth.Min(g => g.Key);\n",
    "var maxCreatedAtDate = createdByMonth.Max(g => g.Key);\n",
    "var maxClosedAtDate = closedByMonth.Max(g => g.Key);\n",
    "var maxDate = maxCreatedAtDate > maxClosedAtDate ?maxCreatedAtDate : maxClosedAtDate;\n",
    "var cursor = minDate;\n",
    "var runningTotal = 0;\n",
    "var issuesCreatedThisMonthByArea = currentYearIssues.Where(issue => issue.CreatedAt >= startOfTheMonth.ToDateTimeUnspecified()).Where(issue => IsBug(issue) && TargetsArea(issue)).GroupBy(issue => GetArea(issue)).ToDictionary(g => g.Key, g => g.Count());\n",
    "var openIssueByArea = currentYearIssues.Where(issue => issue.State == \"open\").Where(issue => IsBug(issue) && TargetsArea(issue)).GroupBy(issue => GetArea(issue)).ToDictionary(g => g.Key, g => g.Count());\n",
    "\n",
    "while (cursor <= maxDate )\n",
    "{\n",
    "    createdByMonth.TryGetValue(cursor, out var openCount);\n",
    "    closedByMonth.TryGetValue(cursor, out var closedCount);\n",
    "    runningTotal += (openCount - closedCount);\n",
    "    openByMonth[cursor] = runningTotal;\n",
    "    cursor = cursor.AddMonths(1);\n",
    "}\n",
    "\n",
    "var issueLifespan = currentYearIssues.Where(IsBug).GroupBy(issue =>\n",
    "            {\n",
    "                var lifeSpan = (issue.ClosedAt ?? today.ToDateTimeUnspecified()) - issue.CreatedAt;\n",
    "                return Math.Max(0, Math.Round(Math.Ceiling(lifeSpan.TotalDays),0));\n",
    "            })\n",
    "            .Where(g => g.Key > 0)\n",
    "            .OrderBy(g => g.Key)\n",
    "            .ToDictionary(g => g.Key, g => g.Count());\n",
    "\n",
    "display(new { \n",
    "    less_then_one_sprint = issueLifespan.Where(i=> i.Key < 21).Select(i => i.Value).Sum(),\n",
    "    less_then_two_sprint = issueLifespan.Where(i=> i.Key >= 21 && i.Key < 42).Select(i => i.Value).Sum(),\n",
    "    more_then_two_sprint = issueLifespan.Where(i=> i.Key >= 42).Select(i => i.Value).Sum()   \n",
    "    });"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Activity dashboard"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "GenericChart.GenericChart ScatterglDict<T1, T2>(Dictionary<T1, T2> dict, string name)\n",
    "{\n",
    "    var trace = new Trace(\"scatter\");\n",
    "    trace.SetValue(\"x\", dict.Select(pair => pair.Key));\n",
    "    trace.SetValue(\"y\",  dict.Select(pair => pair.Value));\n",
    "    trace.SetValue(\"name\", name);\n",
    "    return GenericChart.ofTraceObject(true, trace);\n",
    "}\n",
    "\n",
    "var issueChart = GenericChart.combine( new []{\n",
    "    ScatterglDict(createdByMonth, \"Created\"), \n",
    "    ScatterglDict(openByMonth, \"Open\"), \n",
    "    ScatterglDict(closedByMonth, \"Closed\")}\n",
    ");\n",
    "\n",
    "issueChart.WithTitle(\"Bugs by month\");\n",
    "issueChart.Display();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "// Helper function for some of the below charts.\n",
    "\n",
    "GenericChart.GenericChart BarIEnumerableKeyValuePair<T1,T2>(IEnumerable<KeyValuePair<T1,T2>> seq, string name, string color)\n",
    "{\n",
    "    var trace = new Trace(\"bar\");\n",
    "    trace.SetValue(\"x\", seq.OrderBy(issue => issue.Key).Select(issue => issue.Value));\n",
    "    trace.SetValue(\"y\",  seq.OrderBy(issue => issue.Key).Select(issue => issue.Key));\n",
    "    trace.SetValue(\"name\", name);\n",
    "    return GenericChart.ofTraceObject(true, trace);\n",
    "\n",
    "    // return new Bar\n",
    "    // {\n",
    "    //     name = name,\n",
    "    //     y = seq.OrderBy(issue => issue.Key).Select(issue => issue.Value),\n",
    "    //     x = seq.OrderBy(issue => issue.Key).Select(issue => issue.Key),\n",
    "    //     marker = new Marker{ color = color }  \n",
    "    // };    \n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "var issueLifespanChart =  GenericChart.combine(new[] {\n",
    "    BarIEnumerableKeyValuePair(issueLifespan.Where(issue => issue.Key < 7), \"One week old\", \"green\"), \n",
    "    BarIEnumerableKeyValuePair(issueLifespan.Where(issue => issue.Key >= 7 && issue.Key < 21), \"One Sprint old\", \"yellow\"), \n",
    "    BarIEnumerableKeyValuePair(issueLifespan.Where(issue => issue.Key >= 21), \"More then a Sprint\", \"red\")\n",
    "});\n",
    "\n",
    "\n",
    "issueLifespanChart.WithTitle( \"Bugs by life span\");\n",
    "issueLifespanChart.WithXAxisStyle(Title.init(\"Bugs by life span\"));\n",
    "\n",
    "issueLifespanChart.Display();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "var openIssuesAgeChart = GenericChart.combine(new[] {\n",
    "    BarIEnumerableKeyValuePair(openIssueAge.Where(issue => issue.Value < 7), \"Closed in a week\", \"green\"), \n",
    "    BarIEnumerableKeyValuePair(openIssueAge.Where(issue => issue.Value >= 7 && issue.Value < 21), \"Closed within a sprint\", \"yellow\"), \n",
    "    BarIEnumerableKeyValuePair(openIssueAge.Where(issue => issue.Value >= 21), \"Long standing\", \"red\")\n",
    "});\n",
    "\n",
    "openIssuesAgeChart.WithTitle( \"Open bugs age\");\n",
    "openIssuesAgeChart.WithXAxisStyle(Title.init(\"Number of days a bug stays open\"));\n",
    "\n",
    "openIssuesAgeChart.Display();\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "var createdThisMonthAreaSeries = new Trace(\"pie\");\n",
    "createdThisMonthAreaSeries.SetValue(\"values\", issuesCreatedThisMonthByArea.Select(issue => issue.Value));\n",
    "createdThisMonthAreaSeries.SetValue(\"labels\",  issuesCreatedThisMonthByArea.Select(e => e.Key));\n",
    "\n",
    "\n",
    "var createdArea = Chart.Combine(new[] {GenericChart.ofTraceObject(true, createdThisMonthAreaSeries)});\n",
    "createdArea.WithTitle( \"Bugs created this month by Area\");\n",
    "\n",
    "createdArea.Display();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "var openAreaSeries = new Trace(\"pie\");\n",
    "openAreaSeries.SetValue(\"values\", openIssueByArea.Select(e => e.Value));\n",
    "openAreaSeries.SetValue(\"labels\",  openIssueByArea.Select(e => e.Key));\n",
    "\n",
    "\n",
    "var openArea = Chart.Combine(new[] {GenericChart.ofTraceObject(true, openAreaSeries)});\n",
    "openArea.WithTitle( \"Open bugs by Area\");\n",
    "\n",
    "openArea.Display();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "var prLifespanChart = Chart.Combine(new[] {\n",
    "    BarIEnumerableKeyValuePair(pullRequestLifespan.Where(issue => issue.Key < 7), \"One week\", \"green\"), \n",
    "    BarIEnumerableKeyValuePair(pullRequestLifespan.Where(issue => issue.Key >= 7 && issue.Key < 21), \"One Sprint\", \"yellow\"), \n",
    "    BarIEnumerableKeyValuePair(pullRequestLifespan.Where(issue => issue.Key >= 21), \"More than a Sprint\", \"red\")\n",
    "});\n",
    "\n",
    "prLifespanChart.WithTitle( \"Pull Request by life span\");\n",
    "prLifespanChart.WithXAxisStyle(Title.init(\"Number of days a PR stays open\"));\n",
    "prLifespanChart.WithYAxisStyle(Title.init(\"Number of PR\"));\n",
    "prLifespanChart.Display();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "var forkCreationSeries = new Trace(\"scatter\");\n",
    "forkCreationSeries.SetValue(\"x\", forkCreatedByMonth.Select(g => g.Key ).ToArray());\n",
    "forkCreationSeries.SetValue(\"y\", forkCreatedByMonth.Select(g => g.Count() ).ToArray());\n",
    "forkCreationSeries.SetValue(\"name\", \"created by month\");\n",
    "\n",
    "var forkTotalSeries = new Trace(\"scatter\");\n",
    "forkTotalSeries.SetValue(\"x\", forkCountByMonth.Select(g => g.Date ).ToArray());\n",
    "forkTotalSeries.SetValue(\"y\", forkCountByMonth.Select(g => g.Count ).ToArray());\n",
    "forkTotalSeries.SetValue(\"name\", \"running total\");\n",
    "\n",
    "\n",
    "var forkUpdateSeries = new Trace(\"scatter\");\n",
    "forkUpdateSeries.SetValue(\"x\", forkUpdateByMonth.Select(g => g.Date ).ToArray());\n",
    "forkUpdateSeries.SetValue(\"y\", forkUpdateByMonth.Select(g => g.Count ).ToArray());\n",
    "forkUpdateSeries.SetValue(\"name\", \"last update by month\");\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "var chart = Chart.Combine(new[] {GenericChart.ofTraceObject(true,forkCreationSeries),GenericChart.ofTraceObject(true,forkTotalSeries),GenericChart.ofTraceObject(true,forkUpdateSeries)});\n",
    "chart.WithTitle(\"Fork activity\");\n",
    "chart.Display();"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".NET (C#)",
   "language": "C#",
   "name": ".net-csharp"
  },
  "language_info": {
   "file_extension": ".cs",
   "mimetype": "text/x-csharp",
   "name": "C#",
   "pygments_lexer": "csharp",
   "version": "8.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
